/*
   Copyright 2008, Andres Postiglioni

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
package com.aconcagua.factory {
  import com.aconcagua.beans.BeanDefinition;
  import com.aconcagua.beans.BeanDefinitionException;
  import com.aconcagua.beans.FactoryBean;

  import flash.events.EventDispatcher;
  import flash.utils.Dictionary;
  import flash.utils.getQualifiedClassName;

  import mx.logging.ILogger;
  import mx.logging.Log;
  import mx.utils.NameUtil;

  /**
   * @author Andres Postiglioni
   */
  public class BeanFactorySupport extends EventDispatcher implements BeanFactory {
    private var _definitions: Dictionary = new Dictionary();
    private var _singletons: Dictionary = new Dictionary();
    private var _aliases: Dictionary = new Dictionary();

    private static var _logger: ILogger = Log.getLogger("com.aconcagua.factory.BeanFactorySupport");

    /**
    * Holds a refererence of the beans that are been instantiated.
    * Required to detect cross-references deadlocks
    **/
    private var _instantiationChain: Dictionary = new Dictionary();
    private var _instantiationChainLength: int = 0;
    private var _deferredPropertiesExecutions: Array = new Array(); //Array of functions

    private function getRegisteredSingleton(name: String): * {
      return _singletons[name];
    }

    public function getBean(name: String):* {
      var bean: * = this.resolveBean(name);

      return bean is FactoryBean? FactoryBean(bean).create() : bean;
    }

    protected function resolveBean(id: String): * {
      var beanDefinition: BeanDefinition = this.getBeanDefinition(id);

      if (beanDefinition.isSingleton) {
        if (!this.isInitializedSingleton(beanDefinition.name)) {
          //It has not been registered yet (it's lazy),
          //so we register it.
          this.initializeDefinition(beanDefinition, true);
        }

        return this.getRegisteredSingleton(beanDefinition.name);
      }
      else /* it's prototype */{
        return this.createPrototype(beanDefinition);
      }
    }

    private function createPrototype(beanDefinition: BeanDefinition): * {
      var beanInstance: Object = this.createInstance(beanDefinition);
      this.processProperties(beanDefinition, beanInstance);

      return beanInstance;
    }

    protected function createInstance(definition: BeanDefinition): Object {
      if (_instantiationChain[definition.name] != null) {
        throw new BeanCurrentlyInCreationException(definition.name);
      }

      try {
        _instantiationChain[definition.name] = definition;
        _instantiationChainLength++;

        var instance: Object = definition.createInstance(this);

        delete _instantiationChain[definition.name];
        _instantiationChainLength--;

      _logger.debug("new object created for definition named {0}", definition.name);

        return instance;
      }
      catch(e: Error) {
        throw new BeanCreationException(definition.name, e);
      }

      //This should never execute. Either we return an instance inside the try block,
      //or an exception is thrown from within the catch block.
      //However, the compiler complains if we don't return anything at this point.
      throw new BeanCreationException(
        definition.name,
        new Error("Unexpected execution flow")
      );
    }

    private function processProperties(definition: BeanDefinition, beanInstance: Object): void {
      this.deferPropertiesExecution(definition, beanInstance);

      if (!this.instantiatingNestedBean()) {
        while(_deferredPropertiesExecutions.length > 0) {
          var command: Function = _deferredPropertiesExecutions.shift() as Function;
          command.call(this);
        }
      }
    }

    private function deferPropertiesExecution(definition: BeanDefinition, beanInstance: Object): void {
      _logger.debug("deferring properties execution for bean {0}", definition.name);

      _deferredPropertiesExecutions.push(
        function(): void {
          this.applyProperties(definition, beanInstance);
        }
      );
    }

    protected function applyProperties(definition: BeanDefinition, beanInstance: Object): void {
      _logger.debug("applying properties to bean: " + definition.name);

      definition.applyProperties(beanInstance, this);

      if (beanInstance is BeanFactoryAware) {
        BeanFactoryAware(beanInstance).beanFactory = this;
      }
    }

    //TODO: REVIEW
    private function instantiatingNestedBean(): Boolean {
      return _instantiationChainLength > 0;
    }

    private function isInitializedSingleton(id: String): Boolean {
      return _singletons[id] != null;
    }

    public function containsBean(id: String): Boolean {
      return this.containsDefinition(id);
    }

    private function containsDefinition(name: String): Boolean {
      return _definitions[name] != null;
    }

    /**
    * Adds the given singleton instance to the singleton registry.
    * The instance may have not being fully initialized yet
    **/
    private function registerSingleton(id: String, beanInstance: Object): void {
      _singletons[id] = beanInstance;
    }

    public function registerBeanDefinition(bean: BeanDefinition): void {
      this.validateDefinition(bean);
      if (bean.name == null) {
        bean.name = this.generateNameForDefinition(bean);
      }

      _logger.debug("registering definition for bean: " + bean.name);

      this.addDefinition(bean.name, bean);
      this.initializeDefinition(bean, false);
    }

    public function registerAlias(alias: String, bean: BeanDefinition): void {
      this.addDefinition(alias, bean);

      var aliasesForBean: Array = _aliases[bean.name] as Array;
      if (aliasesForBean == null) {
        _aliases[bean.name] = new Array();
        aliasesForBean = _aliases[bean.name] as Array;
      }

      aliasesForBean.push(alias);
    }

    private function validateDefinition(definition: BeanDefinition): void {
      if (this.containsDefinition(definition.name))
        throw new BeanDefinitionException(definition.name, BeanDefinitionException.DUPLICATE_NAME);
      if (definition.type == null)
        throw new BeanDefinitionException(definition.name, BeanDefinitionException.NO_TYPE);
    }

    private function initializeDefinition(bean: BeanDefinition, initializeLazy: Boolean): void {
      //Instantiate and register singleton instances if they are eager,
      //or if we are forced to initialize lazy definitions.
      //Prototype beans will not be initialized, since instances will be created on demand.
      if (bean.isSingleton && (!bean.lazy || initializeLazy)) {
        _logger.debug("initializing bean: " + bean.name);

        var beanInstance:Object = this.createInstance(bean);
        this.registerSingleton(bean.name, beanInstance);
        this.processProperties(bean, beanInstance);
      }
    }

    protected function getBeanDefinition(name: String): BeanDefinition {
      var beanDefinition: BeanDefinition = _definitions[name];
      if (beanDefinition == null) {
        throw new NoSuchBeanDefinitionException("Undefined bean: " + name);
      }

      return beanDefinition;
    }

    private function addDefinition(name: String, bean: BeanDefinition): void {
      if (this.containsBean(name)) {
        throw new BeanDefinitionException(
          name,
          BeanDefinitionException.DUPLICATE_NAME
        );
      }

      _definitions[name] = bean;
    }

    public function getAliases(name: String): Array {
      return _aliases[name] as Array != null? _aliases[name] : new Array();
    }

    public function getType(name: String): Class {
      return this.getBeanDefinition(name).type;
    }

    public function isPrototype(name: String): Boolean {
      return this.getBeanDefinition(name).isPrototype;
    }

    public function isSingleton(name: String): Boolean {
      return this.getBeanDefinition(name).isSingleton;
    }

    public function isTypeMatch(name: String, targetType: Class): Boolean {
      return this.getBeanDefinition(name).type == targetType;
    }

    private function generateNameForDefinition(definition: BeanDefinition): String {
      return "anonymous-bean:"  + getQualifiedClassName(definition.type) + ":" + NameUtil.createUniqueName(definition);
    }
  }
}